package ru.yandex.jenkins.plugins.debuilder;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.model.BuildListener;
import hudson.model.Environment;
import hudson.model.AbstractBuild;
import hudson.model.Cause;
import hudson.model.Cause.UserIdCause;
import hudson.model.Descriptor;
import hudson.model.Project;
import hudson.scm.SCM;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.DescribableList;
import hudson.util.VariableResolver;
import java.io.IOException;
import java.io.PrintStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jedi.functional.FunctionalPrimitives;
import jedi.functional.Functor;
import net.sf.json.JSONObject;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import ru.yandex.jenkins.plugins.debuilder.DebUtils.Runner;
import static ru.yandex.jenkins.plugins.debuilder.ChangesExtractor.Change;
public class DebianPackageBuilder extends Builder {
public static final String DEBIAN_SOURCE_PACKAGE = "DEBIAN_SOURCE_PACKAGE";
public static final String DEBIAN_PACKAGE_VERSION = "DEBIAN_PACKAGE_VERSION";
public static final String ABORT_MESSAGE = "[{0}] Aborting: {1} ";
private static final String PREFIX = "debian-package-builder";
// location of debian catalog relative to the workspace root
private final String pathToDebian;
private final String nextVersion;
private final boolean generateChangelog;
private final boolean signPackage;
private final boolean buildEvenWhenThereAreNoChanges;
@DataBoundConstructor
public DebianPackageBuilder(String pathToDebian, String nextVersion, Boolean generateChangelog, Boolean signPackage, Boolean buildEvenWhenThereAreNoChanges) {
this.pathToDebian = pathToDebian;
this.nextVersion = nextVersion;
this.generateChangelog = generateChangelog;
this.signPackage = signPackage;
this.buildEvenWhenThereAreNoChanges = buildEvenWhenThereAreNoChanges;
}
public String getPathToDebian() {
return pathToDebian;
}
public String getNextVersion() {
return nextVersion;
}
public boolean isGenerateChangelog() {
return generateChangelog;
}
public boolean isSignPackage() {
return signPackage;
}
public boolean isBuildEvenWhenThereAreNoChanges() {
return buildEvenWhenThereAreNoChanges;
}
@Override
public boolean perform(@SuppressWarnings("rawtypes") AbstractBuild build, Launcher launcher, BuildListener listener) {
PrintStream logger = listener.getLogger();
FilePath workspace = build.getWorkspace();
Runner runner = makeRunner(build, launcher, listener);
try {
String remoteDebian = getRemoteDebian(build, runner);
runner.runCommand("sudo apt-get -y update");
runner.runCommand("sudo apt-get -y install aptitude pbuilder");
importKeys(workspace, runner);
Map<String, String> changelog = parseChangelog(runner, remoteDebian);
String source = changelog.get("Source");
String latestVersion = changelog.get("Version");
String distribution = changelog.get("Distribution");
runner.announce("Determined latest version to be {0}", latestVersion);
if (generateChangelog) {
Pair<VersionHelper, List<Change>> changes = generateChangelog(latestVersion, runner, build, remoteDebian);
if (isTriggeredAutomatically(build) && changes.getRight().isEmpty() && !buildEvenWhenThereAreNoChanges) {
runner.announce("There are no creditable changes for this build - not building package.");
return true;
}
latestVersion = changes.getLeft().toString();
writeChangelog(build, listener, remoteDebian, runner, changes, distribution);
}
runner.runCommand("cd ''{0}'' && sudo /usr/lib/pbuilder/pbuilder-satisfydepends --control control", remoteDebian);
String package_command = String.format("cd '%1$s' && debuild --check-dirname-level 0 --no-tgz-check ", remoteDebian);
if (signPackage) {
package_command += String.format("-k%1$s -p'gpg --no-tty --passphrase %2$s'", getDescriptor().getAccountEmail(), getDescriptor().getPassphrase());
}
else
{
package_command += "-us -uc";
}
runner.runCommand(package_command);
archiveArtifacts(build, runner, latestVersion);
build.addAction(new DebianBadge(latestVersion, remoteDebian));
EnvVars envVars = new EnvVars(DEBIAN_SOURCE_PACKAGE, source, DEBIAN_PACKAGE_VERSION, latestVersion);
build.getEnvironments().add(Environment.create(envVars));
} catch (InterruptedException e) {
logger.println(MessageFormat.format(ABORT_MESSAGE, PREFIX, e.getMessage()));
return false;
} catch (DebianizingException e) {
logger.println(MessageFormat.format(ABORT_MESSAGE, PREFIX, e.getMessage()));
return false;
} catch (IOException e) {
logger.println(MessageFormat.format(ABORT_MESSAGE, PREFIX, e.getMessage()));
return false;
}
return true;
}
@SuppressWarnings("rawtypes") Runner makeRunner(AbstractBuild build, Launcher launcher, BuildListener listener) {
Runner runner = new Runner(build, launcher, listener, PREFIX);
return runner;
}
@SuppressWarnings("rawtypes")
private void archiveArtifacts(AbstractBuild build, Runner runner, String latestVersion) throws IOException, InterruptedException {
FilePath path = build.getWorkspace().child(pathToDebian).child("..");
String mask = "*" + latestVersion + "*.deb";
for (FilePath file:path.list(mask)) {
runner.announce("Archiving file <{0}> as a build artifact", file.getName());
}
path.copyRecursiveTo(mask, new FilePath(build.getArtifactsDir()));
}
@SuppressWarnings("rawtypes")
public String getRemoteDebian(AbstractBuild build, Runner runner) throws DebianizingException {
FilePath workspace = build.getWorkspace();
String expanded;
try {
expanded = build.getEnvironment(runner.getListener()).expand(pathToDebian);
if (expanded.endsWith("debian") || expanded.endsWith("debian/")) {
return workspace.child(expanded).getRemote();
} else {
return workspace.child(expanded).child("debian").getRemote();
}
} catch (IOException cause) {
throw new DebianizingException("Failed to get build environment", cause);
} catch (InterruptedException cause) {
throw new DebianizingException("Failed to get build environment", cause);
}
}
/**
* Parses changelog and updates it with next version and it's changes
*
* @param latestVersion
* @param runner
* @param build
* @param remoteDebian
* @return
* @throws DebianizingException
* @throws InterruptedException
* @throws IOException
*/
@SuppressWarnings({ "rawtypes" }) Pair<VersionHelper, List<Change>> generateChangelog(String latestVersion, Runner runner, AbstractBuild build, String remoteDebian) throws DebianizingException, InterruptedException, IOException {
VersionHelper helper;
EnvVars env = build.getEnvironment(runner.getListener());
String nextVersion = env.expand(this.nextVersion);
if (nextVersion == null || nextVersion.trim().isEmpty()) {
helper = new VersionHelper(latestVersion);
runner.announce("Determined latest revision to be {0}", helper.getRevision());
helper.setMinorVersion(helper.getMinorVersion() + 1);
} else {
helper = new VersionHelper(nextVersion);
}
SCM scm = build.getProject().getScm();
String ourMessage = DebianPackagePublisher.getUsedCommitMessage(build);
List<Change> changes = ChangesExtractor.getChanges(build, runner, scm, remoteDebian, ourMessage, helper);
return new ImmutablePair<VersionHelper, List<Change>>(helper, changes);
}
/**
* Writes down changelog contained in <b>changes</b>
*
* @param build
* @param listener
* @param remoteDebian
* @param runner
* @param changes
* @param previousChangelog
* @throws IOException
* @throws InterruptedException
* @throws DebianizingException
*/
@SuppressWarnings("rawtypes")
private void writeChangelog(AbstractBuild build, BuildListener listener, String remoteDebian, Runner runner, Pair<VersionHelper, List<Change>> changes, String distribution) throws IOException,
InterruptedException, DebianizingException {
String versionMessage = getCausedMessage(build);
String newVersionMessage = Util.replaceMacro(versionMessage, new VariableResolver.ByMap<String>(build.getEnvironment(listener)));
startVersion(runner, remoteDebian, changes.getLeft(), newVersionMessage, distribution);
for (Change change: changes.getRight()) {
addChange(runner, remoteDebian, change, distribution);
}
}
@SuppressWarnings("rawtypes")
private boolean isTriggeredAutomatically (AbstractBuild build) {
for (Object cause: build.getCauses()) {
if (cause instanceof UserIdCause) {
return false;
}
}
return true;
}
/**
* Returns message based on causes of the build
* @param build
* @return
*/
@SuppressWarnings("rawtypes")
private String getCausedMessage(AbstractBuild build) {
String firstPart = "Build #${BUILD_NUMBER}. ";
@SuppressWarnings("unchecked")
List<Cause> causes = build.getCauses();
List<String> causeMessages = FunctionalPrimitives.map(causes, new Functor<Cause, String>() {
@Override
public String execute(Cause value) {
return value.getShortDescription();
}
});
Set<String> uniqueCauses = new HashSet<String>(causeMessages);
return firstPart + FunctionalPrimitives.join(uniqueCauses, ". ") + ".";
}
private String clearMessage(String message) {
return message.replaceAll("\\'", "");
}
private void addChange(Runner runner, String remoteDebian, Change change, String distribution) throws InterruptedException, DebianizingException {
runner.announce("Got changeset entry: {0} by {1}", clearMessage(change.getMessage()), change.getAuthor());
runner.runCommand("export DEBEMAIL={0} && export DEBFULLNAME=''{1}'' && cd ''{2}'' && dch --check-dirname-level 0 --distribution ''{4}'' --append -- ''{3}''", getDescriptor().getAccountEmail(), change.getAuthor(), remoteDebian, clearMessage(change.getMessage()), distribution);
}
private void startVersion(Runner runner, String remoteDebian, VersionHelper helper, String message, String distribution) throws InterruptedException, DebianizingException {
runner.announce("Starting version <{0}> with message <{1}>", helper, clearMessage(message));
runner.runCommand("export DEBEMAIL={0} && export DEBFULLNAME=''{1}'' && cd ''{2}'' && dch --check-dirname-level 0 -b --distribution ''{5}'' --newVersion {3} -- ''{4}''", getDescriptor().getAccountEmail(), getDescriptor().getAccountName(), remoteDebian, helper, clearMessage(message), distribution);
}
/**
* FIXME Doesn't work with multi-line entries
*/
Map<String, String> parseChangelog(Runner runner, String remoteDebian) throws DebianizingException {
String changelogOutput = runner.runCommandForOutput("cd \"{0}\" && dpkg-parsechangelog -lchangelog", remoteDebian);
Map<String, String> changelog = new HashMap<String, String>();
Pattern changelogFormat = Pattern.compile("(\\w+):\\s*(.*)");
for(String row: changelogOutput.split("\n")) {
Matcher matcher = changelogFormat.matcher(row.trim());
if (matcher.matches()) {
changelog.put(matcher.group(1), matcher.group(2));
}
}
return changelog;
}
private void importKeys(FilePath workspace, Runner runner)
throws InterruptedException, DebianizingException, IOException {
if (!runner.runCommandForResult("gpg --list-key {0}", getDescriptor().getAccountEmail())) {
FilePath publicKey = workspace.createTextTempFile("public", "key", getDescriptor().getPublicKey());
runner.runCommand("gpg --import ''{0}''", publicKey.getRemote());
publicKey.delete();
}
if (!runner.runCommandForResult("gpg --list-secret-key {0}", getDescriptor().getAccountEmail())) {
FilePath privateKey = workspace.createTextTempFile("private", "key", getDescriptor().getPrivateKey());
runner.runCommand("gpg --import ''{0}''", privateKey.getRemote());
privateKey.delete();
}
}
@Override
public DescriptorImpl getDescriptor() {
return (DescriptorImpl)super.getDescriptor();
}
@Extension
public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
private String publicKey;
private String privateKey;
private String accountName;
private String accountEmail;
private String passphrase;
public DescriptorImpl() {
load();
}
@Override
public String getDisplayName() {
return "Build debian package";
}
public String getPublicKey() {
return publicKey;
}
@Override
public boolean isApplicable(@SuppressWarnings("rawtypes") Class type) {
return true;
}
@Override
public boolean configure(StaplerRequest staplerRequest, JSONObject json) throws FormException {
setPrivateKey(json.getString("privateKey"));
setPublicKey(json.getString("publicKey"));
setAccountName("Jenkins");
setAccountEmail(json.getString("accountEmail"));
setPassphrase(json.getString("passphrase"));
save();
return true; // indicate that everything is good so far
}
public String getPrivateKey() {
return privateKey;
}
public void setPrivateKey(String privateKey) {
this.privateKey = privateKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public String getAccountEmail() {
return accountEmail;
}
public void setAccountEmail(String accountEmail) {
this.accountEmail = accountEmail;
}
public String getPassphrase() {
return passphrase;
}
public void setPassphrase(String passphrase) {
this.passphrase = passphrase;
}
}
/**
* @param build
* @param runner
* @return all the paths to remote module roots declared in given build by {@link DebianPackageBuilder}s
* @throws DebianizingException
*/
public static Collection<String> getRemoteModules(AbstractBuild<?, ?> build, Runner runner) throws DebianizingException {
ArrayList<String> result = new ArrayList<String>();
for (DebianPackageBuilder builder: getDPBuilders(build)) {
result.add(new FilePath(build.getWorkspace().getChannel(), builder.getRemoteDebian(build, runner)).child("..").getRemote());
}
return result;
}
/**
* @param build
* @return all the {@link DebianPackageBuilder}s participating in this build
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static Collection<DebianPackageBuilder> getDPBuilders(AbstractBuild<?, ?> build) {
ArrayList<DebianPackageBuilder> result = new ArrayList<DebianPackageBuilder>();
if (build.getProject() instanceof Project) {
DescribableList<Builder, Descriptor<Builder>> builders = ((Project)build.getProject()).getBuildersList();
for (Builder builder: builders) {
if (builder instanceof DebianPackageBuilder) {
result.add((DebianPackageBuilder) builder);
}
}
}
return result;
}
}